﻿namespace Cellfish.Test
{
    using System;
    using System.Collections;
    using System.Threading.Tasks;

    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>
    /// Helper functions for better asserts in your unit tests.
    /// </summary>
    public static class Ensure
    {
        /// <summary>
        /// Lets you validate that the exception thrown is what you expect.
        /// </summary>
        /// <param name="action">The action you expect should throw an exception.</param>
        /// <param name="validate">The validation action to make sure the thrown exception is what you expect.</param>
        public static void Throws(Action action, Action<Exception> validate)
        {
            Throws(action, validate, () => Assert.Fail("No exception thrown."));
        }

        /// <summary>
        /// Lets you validate that the exception thrown is what you expect.
        /// </summary>
        /// <param name="action">The action you expect should throw an exception.</param>
        /// <param name="validate">The validation action to make sure the thrown exception is what you expect.</param>
        /// <param name="noException">The action to take place if no exception was thrown.</param>
        public static void Throws(Action action, Action<Exception> validate, Action noException)
        {
            Assert.IsNotNull(action, "An action must be provided.");
            Assert.IsNotNull(validate, "A validation action must be provided.");
            Assert.IsNotNull(noException, "a noException action must be provided.");
            try
            {
                action();
                noException();
            }
            catch (AssertFailedException)
            {
                throw;
            }
            catch (Exception exception)
            {
                validate(exception);
            }
        }

        /// <summary>
        /// Lets you validate that the exception thrown is what you expect.
        /// </summary>
        /// <typeparam name="T">The exception type you expect to be thrown.</typeparam>
        /// <param name="action">The action you expect should throw an exception.</param>
        /// <param name="validate">The validation action to make sure the thrown exception is what you expect.</param>
        public static void Throws<T>(Action action, Action<T> validate) where T : Exception
        {
            Throws(
                action,
                exception =>
                    {
                        Assert.AreEqual(
                            typeof(T).AssemblyQualifiedName,
                            exception.GetType().AssemblyQualifiedName,
                            "The thrown exception ({0}) is not of expected type ({1}).",
                            exception.GetType().Name,
                            typeof(T).Name);
                        validate((T)exception);
                    },
                () => Assert.Fail("No exception was thrown. Expected {0}.", typeof(T).Name));
        }

        /// <summary>
        /// Lets you validate that the exception thrown is what you expect.
        /// </summary>
        /// <typeparam name="T">The exception type you expect to be thrown.</typeparam>
        /// <param name="action">The action you expect should throw an exception.</param>
        /// <param name="message">Error message if thrown exception is not of expected type.</param>
        /// <param name="parameters">Parameters for the message format string</param>
        public static void Throws<T>(Action action, string message, params object[] parameters) where T : Exception
        {
            Throws(
                action,
                exception =>
                Assert.AreEqual(
                    typeof(T).AssemblyQualifiedName,
                    exception.GetType().AssemblyQualifiedName,
                    "The thrown exception ({0}) is not of expected type ({1}). {2}",
                            exception.GetType().Name,
                            typeof(T).Name,
                    string.Format(message, parameters)),
                () =>
                Assert.Fail(
                    "No exception was thrown. Expected {0}. {1}",
                    typeof(T).Name,
                    string.Format(message, parameters)));
        }

        /// <summary>
        /// Lets you validate that the exception thrown is what you expect.
        /// </summary>
        /// <typeparam name="T">The exception type you expect to be thrown.</typeparam>
        /// <param name="action">The action you expect should throw an exception.</param>
        public static void Throws<T>(Action action) where T : Exception
        {
            Throws<T>(action, _ => { });
        }

        /// <summary>
        /// Lets you validate that a task fails with the expected exception.
        /// </summary>
        /// <typeparam name="T">The exception type you expect to be thrown.</typeparam>
        /// <param name="task">The task that should fail.</param>
        /// <param name="message">Error message if thrown exception is not of expected type.</param>
        /// <param name="parameters">Parameters for the message format string</param>
        public static void TaskFails<T>(Task task, string message, params object[] parameters) where T : Exception
        {
            Assert.IsNotNull(task, "You must provide a task.");
            Throws<AggregateException>(
                task.Wait, 
                exception => Assert.AreEqual(
                    typeof(T).AssemblyQualifiedName,
                    exception.InnerException.GetType().AssemblyQualifiedName,
                    "The task failed in a way ({0}) that was not expected ({1}). {2}",
                    exception.InnerException.GetType().Name,
                    typeof(T).Name,
                    string.Format(message, parameters)));
        }

        /// <summary>
        /// Lets you validate that a task fails with the expected exception.
        /// </summary>
        /// <typeparam name="T">The exception type you expect to be thrown.</typeparam>
        /// <param name="task">The task that should fail.</param>
        public static void TaskFails<T>(Task task) where T : Exception
        {
            TaskFails<T>(task, string.Empty);
        }

        /// <summary>
        /// Lets you validate that a task completes (success, failure or canceled) within 
        /// a specified time.
        /// </summary>
        /// <param name="task">The task you want to wait for.</param>
        /// <param name="timeout">The time you want to wait for the task to complete.</param>
        /// <param name="message">Error message if thrown exception is not of expected type.</param>
        /// <param name="parameters">Parameters for the message format string</param>
        public static void TaskCompletes(Task task, TimeSpan timeout, string message, params object[] parameters)
        {
            Assert.IsNotNull(task, "You must provide a task.");
            if (!task.IsCompleted)
            {
                Assert.IsTrue(
                    ((IAsyncResult)task).AsyncWaitHandle.WaitOne(timeout),
                    "Task did not complete within specified time. {0}",
                    string.Format(message, parameters));
            }
        }

        /// <summary>
        /// Lets you validate that a task completes (success, failure or canceled) within 
        /// a specified time.
        /// </summary>
        /// <param name="task">The task you want to wait for.</param>
        /// <param name="timeout">The time you want to wait for the task to complete.</param>
        public static void TaskCompletes(Task task, TimeSpan timeout)
        {
            Ensure.TaskCompletes(task, timeout, string.Empty);
        }

        /// <summary>
        /// Lets you validate that a collection is empty.
        /// </summary>
        /// <param name="collection">The collection.</param>
        /// <param name="message">Error message if collection is not empty.</param>
        /// <param name="parameters">Parameters for the message format string</param>
        public static void Empty(ICollection collection, string message, params object[] parameters)
        {
            Assert.AreEqual(0, collection.Count, message, parameters);
        }

        /// <summary>
        /// Lets you validate that a collection is empty.
        /// </summary>
        /// <param name="collection">The collection.</param>
        public static void Empty(ICollection collection)
        {
            Empty(collection, string.Empty);
        }
    }
}
